﻿using System;
using System.Collections.Generic;
using IGS.Core.Integrators;
using IGS.Core.Systems;
using Moq;
using NUnit.Framework;

namespace IGS.Core.Tests.Integrators
{
    public static class SymplecticEulerIntegratorTests
    {
        [TestFixture]
        public class GivenOneBodyWithInitialConditionsIncludingAcceleration
        {
            [Test]
            public void WhenTheBodyIsPredictedThenPositionShouldBeUpdatedUsingSymplecticEulerIntegration()
            {
                // Arrange
                const double dt = 0.01;
                var body = new Mock<IInteractable>();
                body.SetupGet(b => b.LastPosition).Returns(new Vector(0.5, 0, 0));
                body.SetupGet(b => b.LastVelocity).Returns(new Vector(-0.01, 1, 0));
                var integrator = new SymplecticEulerIntegrator();

                // Act
                integrator.Predict(body.Object, dt);

                // Assert
                body.VerifyGet(b => b.LastPosition, "Body.LastPositionGet");
                body.VerifyGet(b => b.LastVelocity, "Body.LastVelocityGet");
                body.VerifySet(b => b.Position = It.Is<Vector>(v => v.Equals(new Vector(0.4999, 0.01, 0))));
            }

            [Test]
            public void WhenTheBodyIsCorrectedThenVelocityShouldBeUpdatedUsingSymplecticEulerIntegration()
            {
                // Arrange
                const double dt = 0.01;
                var body = new Mock<IInteractable>();
                body.SetupGet(b => b.LastVelocity).Returns(new Vector(0, 1, 0));
                body.SetupProperty(b => b.Acceleration, new Vector(-1, 0, 0));
                var integrator = new SymplecticEulerIntegrator();

                // Act
                integrator.Correct(body.Object, dt);

                // Assert
                body.VerifyGet(b => b.LastVelocity, "Body.LastVelocityGet");
                body.VerifyGet(b => b.Acceleration, "Body.AccelerationGet");
                body.VerifySet(b => b.Velocity = It.Is<Vector>(v => v.Equals(new Vector(-0.01, 1, 0))));
            }

            [Test]
            public void WhenTheBodyIsIntegratedOneOrbitalPeriodShouldConvergePositionWithOrderOne()
            {
                // Arrange
                const double x = 0.5;
                const double mass = 1.0;
                var position = new Vector(x, 0, 0);
                var velocity = new Vector(0, 1, 0);
                var acceleration = new Vector(-1, 0, 0);
                var orbitalPeriod = 2 * Math.PI * Math.Sqrt(Math.Pow(2 * x, 3) / 2 * mass);
                var integrator = new SymplecticEulerIntegrator();
                const int iterations = 11;
                const int rateOfConvergence = 2;

                var deltas = new List<double>();
                for (int n = 0; n < iterations; n++)
                {
                    var steps = 10 * Math.Pow(2, n);
                    var dt = orbitalPeriod / steps;
                    var body = new Body(mass, position, velocity) { Acceleration = acceleration };

                    // Act
                    integrator.Predict(body, dt);
                    integrator.Correct(body, dt);
                    deltas.Add(body.Position.Length - x);
                }

                RateOfConvergenceAnalysis.AssertConvergence(deltas, rateOfConvergence);
            }

            [Test]
            public void WhenTheBodyIsIntegratedOneOrbitalPeriodShouldConvergeVelocityWithOrderOne()
            {
                // Arrange
                const double mass = 1.0;
                const double x = 0.5;
                const int v = 1;
                var position = new Vector(x, 0, 0);
                var velocity = new Vector(0, v, 0);
                var acceleration = new Vector(-1, 0, 0);
                var orbitalPeriod = 2 * Math.PI * Math.Sqrt(Math.Pow(2 * x, 3) / 2 * mass);
                var integrator = new SymplecticEulerIntegrator();
                const int iterations = 20;
                const int rateOfConvergence = 2;

                var deltas = new List<double>();
                for (int n = 0; n < iterations; n++)
                {
                    var steps = 10 * Math.Pow(2, n);
                    var dt = orbitalPeriod / steps;
                    var body = new Body(mass, position, velocity) { Acceleration = acceleration };

                    // Act
                    integrator.Predict(body, dt);
                    integrator.Correct(body, dt);
                    deltas.Add(body.Velocity.Length - v);
                }

                RateOfConvergenceAnalysis.AssertConvergence(deltas, rateOfConvergence);
            }
        }
    }
}
